package com.jeremyfeinstein.conference;

/**
 * Created by sunxunchao on 2018/10/18.
 */

import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.SurfaceTexture;
import android.opengl.GLES20;
import android.opengl.GLSurfaceView;
import android.util.Log;

import com.intel.webrtc.base.Stream;

import org.webrtc.GlRectDrawer;
import org.webrtc.GlTextureFrameBuffer;
import org.webrtc.GlUtil;
import org.webrtc.RendererCommon;
import org.webrtc.VideoRenderer;

import java.util.Timer;
import java.util.TimerTask;

import javax.microedition.khronos.egl.EGL10;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.egl.EGLContext;
import javax.microedition.khronos.opengles.GL10;

/**
 * 自定义webrtc视频渲染器
 */
public class CustomSurfaceRenderer implements GLSurfaceView.Renderer {

    private static final String TAG = "CustomSurfaceRenderer";
    private GLSurfaceView surface;
    private static EGLContext eglContext = null;
    //    private boolean onSurfaceCreatedCalled;
    private int screenWidth;
    private int screenHeight;
    private GlRectDrawer drawer;
    private static Thread renderFrameThread;
    private static Thread drawThread;
    private YuvImageRenderer yuvImageRenderer;

    public CustomSurfaceRenderer(GLSurfaceView surface) {
        this.surface = surface;
        surface.setPreserveEGLContextOnPause(true);
        surface.setEGLContextClientVersion(2);
        surface.setRenderer(this);
        surface.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);
    }

    public static synchronized EGLContext getEGLContext() {
        return eglContext;
    }

    public synchronized void dispose() {
        Log.d(TAG, "VideoRendererGui.dispose");
        yuvImageRenderer.release();

        renderFrameThread = null;
        drawThread = null;
        this.surface = null;
        eglContext = null;
    }

    public synchronized void stopFrameDetection() {
        if (yuvImageRenderer != null) {
            yuvImageRenderer.stop();
        }
    }

    public synchronized void startFrameDetection() {
        if (yuvImageRenderer != null) {
            yuvImageRenderer.start();
        }
    }

    public synchronized void cancel() {
        Log.d(TAG, "VideoRendererGui.cancel");
        if (yuvImageRenderer != null) {
            yuvImageRenderer.cancelTimer();
        }
    }

    public synchronized void pause() {
        if (yuvImageRenderer != null) {
            yuvImageRenderer.pause();
        }
    }

    public synchronized void resume() {
        if (yuvImageRenderer != null) {
            yuvImageRenderer.resume();
        }
    }

    public synchronized void start() {
        Log.d(TAG, "VideoRendererGui.cancel");
        if (yuvImageRenderer != null) {
            yuvImageRenderer.startTimer();
        }
    }

    public boolean isNewFrame() {
        if (yuvImageRenderer != null) {
            return yuvImageRenderer.isNewFrame;
        }
        return false;
    }


    public Stream.VideoRendererInterface createVideoRenderer() {
        return this.createVideoRenderer(0, 0, 100, 100, RendererCommon.ScalingType.SCALE_ASPECT_FILL, false, null);
    }
    public Stream.VideoRendererInterface createVideoRenderer(OnVideoFrameInfoListener onVideoFrameInfoListener) {
        return this.createVideoRenderer(0, 0, 100, 100, RendererCommon.ScalingType.SCALE_ASPECT_FILL, false, onVideoFrameInfoListener);
    }

    public Stream.VideoRendererInterface createVideoRenderer(int x, int y, int width, int height, RendererCommon.ScalingType scalingType, boolean mirror, OnVideoFrameInfoListener onVideoFrameInfoListener) {
        return this.create(x, y, width, height, scalingType, mirror, onVideoFrameInfoListener);
    }

    public synchronized YuvImageRenderer create(int x, int y, int width, int height, RendererCommon.ScalingType scalingType, boolean mirror, OnVideoFrameInfoListener onVideoFrameInfoListener) {
        if (x >= 0 && x <= 100 && y >= 0 && y <= 100 && width >= 0 && width <= 100 && height >= 0 && height <= 100 && x + width <= 100 && y + height <= 100) {
            yuvImageRenderer = new YuvImageRenderer(this.surface, 1, x, y, width, height, scalingType, mirror, onVideoFrameInfoListener);
            return yuvImageRenderer;
        } else {
            throw new RuntimeException("Incorrect window parameters.");
        }
    }

    public synchronized void update(VideoRenderer.Callbacks renderer, int x, int y, int width, int height, RendererCommon.ScalingType scalingType, boolean mirror) {
        Log.d(TAG, "VideoRendererGui.update");
        if (yuvImageRenderer != null) {
            yuvImageRenderer.setPosition(x, y, width, height, scalingType, mirror);
        }
    }

    public synchronized void setRendererEvents(VideoRenderer.Callbacks renderer, RendererCommon.RendererEvents rendererEvents) {
        Log.d(TAG, "VideoRendererGui.setRendererEvents");
        if (yuvImageRenderer != null) {
            yuvImageRenderer.rendererEvents = rendererEvents;
        }
    }

    public synchronized void reset(VideoRenderer.Callbacks renderer) {
        Log.d(TAG, "VideoRendererGui.reset");
        if (yuvImageRenderer != null) {
            yuvImageRenderer.reset();
        }
    }

    private static void printStackTrace(Thread thread, String threadName) {
        if (thread != null) {
            StackTraceElement[] stackTraces = thread.getStackTrace();
            if (stackTraces.length > 0) {
                Log.d(TAG, threadName + " stacks trace:");
                StackTraceElement[] arr$ = stackTraces;
                int len$ = stackTraces.length;

                for (int i$ = 0; i$ < len$; ++i$) {
                    StackTraceElement stackTrace = arr$[i$];
                    Log.d(TAG, stackTrace.toString());
                }
            }
        }

    }

    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        Log.d(TAG, "onSurfaceCreated: VideoRendererGui.onSurfaceCreated");
        synchronized (this) {
            eglContext = ((EGL10) EGLContext.getEGL()).eglGetCurrentContext();
            Log.d(TAG, "VideoRendererGui EGL Context: " + eglContext);
        }
        this.drawer = new GlRectDrawer();
        if (yuvImageRenderer != null) {
            yuvImageRenderer.createTextures();
        }

        GlUtil.checkNoGLES2Error("onSurfaceCreated done");
        GLES20.glPixelStorei(3317, 1);
        GLES20.glClearColor(0.15F, 0.15F, 0.15F, 1.0F);
    }

    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        Log.d(TAG, "VideoRendererGui.onSurfaceChanged: " + width + " x " + height + "  ");
        this.screenWidth = width;
        this.screenHeight = height;
        if (yuvImageRenderer != null) {
            yuvImageRenderer.setScreenSize(this.screenWidth, this.screenHeight);
//            yuvImageRenderer.setScreenSize(1280, 720);
        }

    }

    @Override
    public void onDrawFrame(GL10 gl) {
        if (gl.glGetError() != GL10.GL_NO_ERROR) {
            Log.d(TAG, "onDrawFrame: opengl error" + gl.glGetError());
        }
        if (drawThread == null) {
            drawThread = Thread.currentThread();
        }

        GLES20.glViewport(0, 0, this.screenWidth, this.screenHeight);
        GLES20.glClear(16384);
        if (yuvImageRenderer != null) {
//            Log.d(TAG, "onDrawFrame: start");
            yuvImageRenderer.draw(this.drawer);
        }
    }


    private static class YuvImageRenderer implements Stream.VideoRendererInterface {
        private GLSurfaceView surface;
        private int id;
        private int[] yuvTextures;
        private GlTextureFrameBuffer textureCopy;
        private VideoRenderer.I420Frame pendingFrame;
        private final Object pendingFrameLock;
        private RendererType rendererType;
        private RendererCommon.ScalingType scalingType;
        private boolean mirror;
        private RendererCommon.RendererEvents rendererEvents;
        boolean seenFrame;
        private int framesReceived;
        private int framesDropped;
        private int framesRendered;
        private long startTimeNs;
        private long drawTimeNs;
        private long copyTimeNs;
        private final Rect layoutInPercentage;
        private final Rect displayLayout;
        private float[] layoutMatrix;
        private boolean updateLayoutProperties;
        private final Object updateLayoutLock;
        private float[] rotatedSamplingMatrix;
        private int screenWidth;
        private int screenHeight;
        private int videoWidth;
        private int videoHeight;
        private int rotationDegree;
        private boolean blackFrame;
        private OnVideoFrameInfoListener onVideoFrameInfoListener;

        private Timer frameTimer;
        private TimerTask frameTimerTask;

        private boolean isNewFrame;

        private YuvImageRenderer(GLSurfaceView surface, int id, int x, int y, int width, int height, RendererCommon.ScalingType scalingType, boolean mirror, OnVideoFrameInfoListener onVideoFrameInfoListener) {
            this.yuvTextures = new int[]{0, 0, 0};
            this.pendingFrameLock = new Object();
            this.startTimeNs = -1L;
            this.displayLayout = new Rect();
            this.updateLayoutLock = new Object();
            this.blackFrame = false;
            Log.d(TAG, "YuvImageRenderer.Create id: " + id);
            this.surface = surface;
            this.id = id;
            this.scalingType = scalingType;
            this.mirror = mirror;
            this.layoutInPercentage = new Rect(x, y, Math.min(100, x + width), Math.min(100, y + height));
            this.updateLayoutProperties = false;
            this.rotationDegree = 0;
            this.onVideoFrameInfoListener = onVideoFrameInfoListener;
            Log.d(TAG, "YuvImageRenderer: ");
//            startTimer();
        }

        public synchronized void reset() {
            this.seenFrame = false;
        }

        private synchronized void release() {
            this.surface = null;
            Object var1 = this.pendingFrameLock;
            synchronized (this.pendingFrameLock) {
                if (this.pendingFrame != null) {
                    VideoRenderer.renderFrameDone(this.pendingFrame);
                    this.pendingFrame = null;
                }
            }
        }

        private synchronized void startTimer() {
            frameTimer = new Timer();
            frameTimerTask = new TimerTask() {
                @Override
                public void run() {
                    if (!isStop) {
                        if (isNewFrame) {
                            logStatistics();
                        }
                    }
                }
            };
            frameTimer.schedule(frameTimerTask, 3000, 3000);
        }

        private synchronized void cancelTimer() {
            if (frameTimerTask != null) {
                frameTimerTask.cancel();
            }
            if (frameTimer != null) {
                frameTimer.cancel();
            }
            frameTimerTask = null;
            frameTimer = null;
        }

        private boolean isPause = false;

        private void pause() {
            isPause = true;
        }

        private void resume() {
            isPause = false;
        }

        private boolean isStop = false;

        private synchronized void stop() {
            isStop = true;
        }

        private synchronized void start() {
            isStop = false;
        }

        private void createTextures() {
            Log.d(TAG, "  YuvImageRenderer.createTextures " + this.id + " on GL thread:" + Thread.currentThread().getId());

            for (int i = 0; i < 3; ++i) {
                this.yuvTextures[i] = GlUtil.generateTexture(3553);
            }

            this.textureCopy = new GlTextureFrameBuffer(6407);
        }

        private void updateLayoutMatrix() {
            Object var1 = this.updateLayoutLock;
            synchronized (this.updateLayoutLock) {
                if (this.updateLayoutProperties) {
                    this.displayLayout.set((this.screenWidth * this.layoutInPercentage.left + 99) / 100, (this.screenHeight * this.layoutInPercentage.top + 99) / 100, this.screenWidth * this.layoutInPercentage.right / 100, this.screenHeight * this.layoutInPercentage.bottom / 100);
                    Log.d(TAG, "ID: " + this.id + ". AdjustTextureCoords. Allowed display size: " + this.displayLayout.width() + " x " + this.displayLayout.height() + ". Video: " + this.videoWidth + " x " + this.videoHeight + ". Rotation: " + this.rotationDegree + ". Mirror: " + this.mirror);
                    float videoAspectRatio = this.rotationDegree % 180 == 0 ? (float) this.videoWidth / (float) this.videoHeight : (float) this.videoHeight / (float) this.videoWidth;
                    Point displaySize = RendererCommon.getDisplaySize(this.scalingType, videoAspectRatio, this.displayLayout.width(), this.displayLayout.height());
                    this.displayLayout.inset((this.displayLayout.width() - displaySize.x) / 2, (this.displayLayout.height() - displaySize.y) / 2);
                    Log.d(TAG, "  Adjusted display size: " + this.displayLayout.width() + " x " + this.displayLayout.height());
                    this.layoutMatrix = RendererCommon.getLayoutMatrix(this.mirror, videoAspectRatio, (float) this.displayLayout.width() / (float) this.displayLayout.height());
                    this.updateLayoutProperties = false;
                    Log.d(TAG, "  AdjustTextureCoords done");
                }
            }
        }

        private void draw(GlRectDrawer drawer) {
//            Log.d(TAG, "draw: seenFrame=>" + seenFrame);
            if (this.seenFrame) {
//                Log.d(TAG, "draw: blackFrame=>" + blackFrame);
                if (this.blackFrame) {
                    GLES20.glClear(16384);
                    this.blackFrame = false;
                } else {
                    long now = System.nanoTime();
                    synchronized (this.pendingFrameLock) {
                        isNewFrame = this.pendingFrame != null;
                        if (isNewFrame && this.startTimeNs == -1L) {
                            this.startTimeNs = now;
                        }

                        if (isNewFrame) {
                            if (this.pendingFrame.yuvFrame) {
                                this.rendererType = RendererType.RENDERER_YUV;
                                drawer.uploadYuvData(this.yuvTextures, this.pendingFrame.width, this.pendingFrame.height, this.pendingFrame.yuvStrides, this.pendingFrame.yuvPlanes);
                                float[] samplingMatrix = RendererCommon.verticalFlipMatrix();
                                this.rotatedSamplingMatrix = RendererCommon.rotateTextureMatrix(samplingMatrix, (float) this.pendingFrame.rotationDegree);
                            } else {
                                this.rendererType = RendererType.RENDERER_TEXTURE;
                                SurfaceTexture surfaceTexture = (SurfaceTexture) this.pendingFrame.textureObject;
                                surfaceTexture.updateTexImage();
                                float[] samplingMatrix = new float[16];
                                surfaceTexture.getTransformMatrix(samplingMatrix);
                                this.rotatedSamplingMatrix = RendererCommon.rotateTextureMatrix(samplingMatrix, (float) this.pendingFrame.rotationDegree);
                                this.textureCopy.setSize(this.pendingFrame.rotatedWidth(), this.pendingFrame.rotatedHeight());
                                GLES20.glBindFramebuffer(36160, this.textureCopy.getFrameBufferId());
                                GlUtil.checkNoGLES2Error("glBindFramebuffer");
                                GLES20.glViewport(0, 0, this.textureCopy.getWidth(), this.textureCopy.getHeight());
                                drawer.drawOes(this.pendingFrame.textureId, this.rotatedSamplingMatrix);
                                this.rotatedSamplingMatrix = RendererCommon.identityMatrix();
                                GLES20.glBindFramebuffer(36160, 0);
                            }

                            this.copyTimeNs += System.nanoTime() - now;
                            VideoRenderer.renderFrameDone(this.pendingFrame);
                            this.pendingFrame = null;
                        }
                    }

                    GLES20.glViewport(this.displayLayout.left, this.screenHeight - this.displayLayout.bottom, this.displayLayout.width(), this.displayLayout.height());
                    this.updateLayoutMatrix();
                    float[] texMatrix = RendererCommon.multiplyMatrices(this.rotatedSamplingMatrix, this.layoutMatrix);
                    if (this.rendererType == RendererType.RENDERER_YUV) {
                        drawer.drawYuv(this.yuvTextures, texMatrix);
                    } else {
                        drawer.drawRgb(this.textureCopy.getTextureId(), texMatrix);
                    }

                    if (isNewFrame) {
                        ++this.framesRendered;
                        this.drawTimeNs += System.nanoTime() - now;

//                        if (this.framesRendered % 300 == 0) {
//                            this.logStatistics();
//                        }

                    }
                }
            }
        }

        public void logStatistics() {
            long timeSinceFirstFrameNs = System.nanoTime() - this.startTimeNs;
            Log.v(TAG, "ID: " + this.id + ". Type: " + this.rendererType + ". Frames received: " + this.framesReceived + ". Dropped: " + this.framesDropped + ". Rendered: " + this.framesRendered);
            if (this.framesReceived > 0 && this.framesRendered > 0) {
                Log.v(TAG, "Duration: " + (int) ((double) timeSinceFirstFrameNs / 1000000.0D) + " ms. FPS: " + (double) ((float) this.framesRendered) * 1.0E9D / (double) timeSinceFirstFrameNs);
                Log.v(TAG, "Draw time: " + (int) (this.drawTimeNs / (long) (1000 * this.framesRendered)) + " us. Copy time: " + (int) (this.copyTimeNs / (long) (1000 * this.framesReceived)) + " us");
                if (onVideoFrameInfoListener != null) {
                    onVideoFrameInfoListener.onVideoFrameInfo(this.framesReceived, this.framesDropped, this.framesRendered, (double) ((float) this.framesRendered) * 1.0E9D / (double) timeSinceFirstFrameNs, (int) ((double) timeSinceFirstFrameNs / 1000000.0D));
                }
            }
        }

        public void setScreenSize(int screenWidth, int screenHeight) {
            Object var3 = this.updateLayoutLock;
            synchronized (this.updateLayoutLock) {
                if (screenWidth != this.screenWidth || screenHeight != this.screenHeight) {
                    Log.d(TAG, "ID: " + this.id + ". YuvImageRenderer.setScreenSize: " + screenWidth + " x " + screenHeight);
                    this.screenWidth = screenWidth;
                    this.screenHeight = screenHeight;
                    this.updateLayoutProperties = true;
                }
            }
        }

        public void setPosition(int x, int y, int width, int height, RendererCommon.ScalingType scalingType, boolean mirror) {
            Rect layoutInPercentage = new Rect(x, y, Math.min(100, x + width), Math.min(100, y + height));
            Object var8 = this.updateLayoutLock;
            synchronized (this.updateLayoutLock) {
                if (!layoutInPercentage.equals(this.layoutInPercentage) || scalingType != this.scalingType || mirror != this.mirror) {
                    Log.d(TAG, "ID: " + this.id + ". YuvImageRenderer.setPosition: (" + x + ", " + y + ") " + width + " x " + height + ". Scaling: " + scalingType + ". Mirror: " + mirror);
                    this.layoutInPercentage.set(layoutInPercentage);
                    this.scalingType = scalingType;
                    this.mirror = mirror;
                    this.updateLayoutProperties = true;
                }
            }
        }

        private void setSize(int videoWidth, int videoHeight, int rotation) {
            if (videoWidth != this.videoWidth || videoHeight != this.videoHeight || rotation != this.rotationDegree) {
                if (this.rendererEvents != null) {
                    Log.d(TAG, "ID: " + this.id + ". Reporting frame resolution changed to " + videoWidth + " x " + videoHeight);
                    this.rendererEvents.onFrameResolutionChanged(videoWidth, videoHeight, rotation);
                }

                Object var4 = this.updateLayoutLock;
                synchronized (this.updateLayoutLock) {
                    Log.d(TAG, "ID: " + this.id + ". YuvImageRenderer.setSize: " + videoWidth + " x " + videoHeight + " rotation " + rotation);
                    this.videoWidth = videoWidth;
                    this.videoHeight = videoHeight;
                    this.rotationDegree = rotation;
                    this.updateLayoutProperties = true;
                    Log.d(TAG, "  YuvImageRenderer.setSize done.");
                }
            }
        }

        public synchronized void renderFrame(VideoRenderer.I420Frame frame) {
//            Log.d(TAG, "renderFrame: start");
            if (this.surface == null) {
                VideoRenderer.renderFrameDone(frame);
            } else {
                if (renderFrameThread == null) {
                    renderFrameThread = Thread.currentThread();
                }

                if (!this.seenFrame && this.rendererEvents != null) {
                    Log.d(TAG, "ID: " + this.id + ". Reporting first rendered frame.");
                    this.rendererEvents.onFirstFrameRendered();
                }

                ++this.framesReceived;
                Object var2 = this.pendingFrameLock;
                synchronized (this.pendingFrameLock) {
                    if (frame.yuvFrame && (frame.yuvStrides[0] < frame.width || frame.yuvStrides[1] < frame.width / 2 || frame.yuvStrides[2] < frame.width / 2)) {
                        Log.e(TAG, "Incorrect strides " + frame.yuvStrides[0] + ", " + frame.yuvStrides[1] + ", " + frame.yuvStrides[2]);
                        VideoRenderer.renderFrameDone(frame);
                        return;
                    }

                    if (this.pendingFrame != null) {
                        ++this.framesDropped;
                        VideoRenderer.renderFrameDone(frame);
                        this.seenFrame = true;
                        return;
                    }

                    this.pendingFrame = frame;
                }

                this.setSize(frame.width, frame.height, frame.rotationDegree);
                this.seenFrame = true;
//                Log.d(TAG, "renderFrame: requestRender");
                if (isPause) return;
                this.surface.requestRender();
            }
        }

        public synchronized void cleanFrame() {
            if (!this.blackFrame) {
                this.blackFrame = true;
                this.surface.requestRender();
            }
        }

        private static enum RendererType {
            RENDERER_YUV,
            RENDERER_TEXTURE;

            private RendererType() {
            }
        }
    }

    public interface OnVideoFrameInfoListener {
        void onVideoFrameInfo(int receivedFrame, int droppedFrame, int renderedFrame, double fps, int duration);
    }
}
